#!/usr/bin/env python
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

DOCUMENTATION = """
---
module: ec2_acl
short_description: Create or delete AWS Network ACLs.
description:
  - Can create or delete AwS Network ACLs.
version_added: "1.8"
author: Edward Zarecor
options:
  state:
    description:
      - create, update or delete the acl
    required: true
    choices: ['present', 'absent']
  name:
    description:
      - Unique name for acl
    required: true
  vpc_id:
    description:
      - The VPC that this acl belongs to
    required: true
    default: null
extends_documentation_fragment: aws
"""

EXAMPLES = '''
- ec2_acl:
    name: public-acls
    state: present
    vpc_id: 'vpc-abababab'

'''

from ansible.module_utils.basic import *
from ansible.module_utils.ec2 import *
import sys
try:
    import boto.vpc
except ImportError:
    print "failed=True msg={0}".format(sys.executable)
    #print "failed=True msg='boto required for this module'"
    sys.exit(1)

from boto.exception import NoAuthHandlerFound

PROTOCOL_NUMBERS = {"ICMP": 1, "TCP": 6, "UDP": 17, "ALL": -1 }


class DuplicateAclError(Exception):
    pass


class ACLManager():

    def __init__(self, connection, vpc_id, acl_name, rules, tags=[]):
        self.connection = connection
        self.vpc_id = vpc_id
        self.acl_name = acl_name
        self.rules = rules
        self.tags = tags
        self.acl = None

    def get_acl(self):

        if not self.acl:
            results = self.connection.get_all_network_acls(filters={"vpc_id": self.vpc_id, "tag:Name": self.acl_name})

            if len(results) == 1:
                self.acl = results[0]
            elif len(results) > 1:
                raise DuplicateAclError("Found multiple network acls name {0} in vpc with id {1}".
                                        format(self.acl_name, self.vpc_id))
            else:
                # Does exist yet
                pass

        return self.acl

    def create_acl(self):
        self.acl = self.connection.create_network_acl(self.vpc_id)
        changed = True
        self.do_tags()
        return changed

    def update_acl(self):
        changed = False
        self.update_rules()
        self.do_tags()
        return changed

    # TODO refactor out repitition
    def update_rules(self):

        current_ingress = [x.rule_number for x in self.acl.network_acl_entries if x.egress == 'false']
        current_egress = [x.rule_number for x in self.acl.network_acl_entries if x.egress == 'true']

        modified_ingress = []
        modified_egress = []

        for rule in self.rules:
            egress = True if rule['type'] == "egress" else False
            protocol = PROTOCOL_NUMBERS[rule['protocol'].upper()]

            if not egress:
                if rule['number'] not in current_ingress:
                    # new rule
                    self.connection.create_network_acl_entry(
                        self.acl.id,
                        rule['number'],
                        protocol,
                        rule['rule_action'],
                        rule['cidr_block'],
                        egress=egress,
                        port_range_from=rule['from_port'],
                        port_range_to=rule['to_port'])
                else:
                    # blindly replace rather than attempting
                    # to determine in the entry has changed
                    modified_ingress.append(rule['number'])
                    self.connection.replace_network_acl_entry (
                        self.acl.id,
                        rule['number'],
                        protocol,
                        rule['rule_action'],
                        rule['cidr_block'],
                        egress=egress,
                        port_range_from=rule['from_port'],
                        port_range_to=rule['to_port'])
            else:
                if rule['number'] not in current_egress:
                    # new rule
                    self.connection.create_network_acl_entry(
                        self.acl.id,
                        rule['number'],
                        protocol,
                        rule['rule_action'],
                        rule['cidr_block'],
                        egress=egress,
                        port_range_from=rule['from_port'],
                        port_range_to=rule['to_port'])
                else:
                    # blindly replace rather than attempting
                    # to determine in the entry has changed
                    modified_egress.append(rule['number'])
                    self.connection.replace_network_acl_entry (
                        self.acl.id,
                        rule['number'],
                        protocol,
                        rule['rule_action'],
                        rule['cidr_block'],
                        egress=egress,
                        port_range_from=rule['from_port'],
                        port_range_to=rule['to_port'])

        removed_ingress_rule_numbers = [ c for c in current_ingress if c not in modified_ingress ]
        removed_egress_rule_numbers = [ c for c in current_egress if c not in modified_egress ]

        for number in removed_ingress_rule_numbers:
            n = int(number)
            # reserved range for AWS
            if n <  32767:
                self.connection.delete_network_acl_entry(self.acl.id, n, False)

        for number in removed_egress_rule_numbers:
            n = int(number)
            # reserved range for AWS
            if n <  32767:
                self.connection.delete_network_acl_entry(self.acl.id, n, True)


    def create_rules(self):
        if self.rules is None:
            return
        for rule in self.rules:
            egress = True if rule['type'] == "egress" else False
            protocol = PROTOCOL_NUMBERS[rule['protocol'].upper()]
            self.connection.create_network_acl_entry(
                self.acl.id,
                rule['number'],
                protocol,
                rule['rule_action'],
                rule['cidr_block'],
                egress=egress,
                port_range_from=rule['from_port'],
                port_range_to=rule['to_port'])

    def do_tags(self):

        tags = {'Name': self.acl_name}
        if self.tags:
            for tag in self.tags:
                tags[tag['key']] = tag['value']
        self.get_acl().add_tags(tags)

    def present(self):

        existing = self.get_acl()

        if not existing:
            changed = self.create_acl()
            self.create_rules()
        else:
            changed = self.update_acl()

        results = dict(changed=changed,
                       id=self.acl.id,
                       name=self.acl_name,
                       entries=self.rules)

        return results

    def absent(self):
        acl = self.get_acl()
        changed = False

        if acl:
            changed = self.connection.delete_network_acl(acl.id)
        results = dict(changed=changed,
                       id=self.acl.id,
                       name=self.acl_name)

        return results


def main():
    argument_spec = ec2_argument_spec()
    argument_spec.update(
        dict(
            name=dict(required=True, type='str'),
            state=dict(default='present', choices=['present', 'absent']),
            vpc_id=dict(required=True, type='str'),
            rules=dict(type='list'),
            tags=dict(type='list'),
        )
    )

    module = AnsibleModule(argument_spec=argument_spec)

    ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
    profile = module.params.get('profile')
    if region:
        try:
            connection = boto.vpc.connect_to_region(region, profile_name=profile)
        except boto.exception.NoAuthHandlerFound, e:
            module.fail_json(msg=str(e))
    else:
        module.fail_json(msg="region must be specified")

    vpc_id = module.params.get('vpc_id')
    acl_name = module.params.get('name')
    rules_in = module.params.get('rules')
    tags = module.params.get('tags')

    manager = ACLManager(connection, vpc_id, acl_name, rules_in, tags)

    state = module.params.get('state')

    results = dict()

    if state == 'present':
        results = manager.present()
    elif state == 'absent':
        results = manager.absent()

    module.exit_json(**results)

main()
